import Callout from '@/components/Callout';
import Details from '@/components/Details';
import {CourseVideoBanner} from '@/components/CourseBanner';

# Proxy / middleware

The `next-intl` middleware can be created via `createMiddleware`.

It receives a [`routing`](/docs/routing/configuration#define-routing) configuration and takes care of:

1. Locale negotiation
2. Applying relevant redirects & rewrites
3. Providing [alternate links](/docs/routing/configuration#alternate-links) for search engines

**Example:**

```tsx filename="proxy.ts"
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  // Match all pathnames except for
  // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
  // - … the ones containing a dot (e.g. `favicon.ico`)
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};
```

**Note:** `proxy.ts` was called `middleware.ts` up until Next.js 16.

## Locale detection [#locale-detection]

The locale is negotiated based on your routing configuration, taking into account your settings for [`localePrefix`](/docs/routing/configuration#locale-prefix), [`domains`](/docs/routing/configuration#domains), [`localeDetection`](/docs/routing/configuration#locale-detection), and [`localeCookie`](/docs/routing/configuration#locale-cookie).

### Prefix-based routing (default) [#location-detection-prefix]

Prefer to watch a video?

<CourseVideoBanner
  className="mt-2"
  href="https://learn.next-intl.dev/chapters/06-routing/07-prefix-based"
  title="Prefix-based routing"
/>

By default, [prefix-based routing](/docs/routing/configuration#locale-prefix) is used to determine the locale of a request.

In this case, the locale is detected based on these priorities:

1. A locale prefix is present in the pathname (e.g. `/en/about`)
2. A cookie is present that contains a previously detected locale
3. A locale can be matched based on the [`accept-language` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language)
4. As a last resort, the `defaultLocale` is used

To change the locale, users can visit a prefixed route. This will take precedence over a previously matched locale that is saved in a cookie or the `accept-language` header and will update a previous cookie value.

**Example workflow:**

1. A user requests `/` and based on the `accept-language` header, the `en` locale is matched.
2. The user is redirected to `/en`.
3. The app renders `<Link locale="de" href="/">Switch to German</Link>` to allow the user to change the locale to `de`.
4. When the user clicks on the link, a request to `/de` is initiated.
5. The middleware will add a cookie to remember the preference for the `de` locale.
6. The user later requests `/` again and the middleware will redirect to `/de` based on the cookie.

<Details id="accept-language-matching">
<summary>Which algorithm is used to match the accept-language header against the available locales?</summary>

To determine the best-matching locale based on the available options from your app, the middleware uses the "best fit" algorithm of [`@formatjs/intl-localematcher`](https://www.npmjs.com/package/@formatjs/intl-localematcher). This algorithm is expected to provide better results than the more conservative "lookup" algorithm that is specified in [RFC 4647](https://www.rfc-editor.org/rfc/rfc4647.html#section-3.4).

To illustrate this with an example, let's consider your app supports these locales:

1. `en-US`
2. `de-DE`

The "lookup" algorithm works by progressively removing subtags from the user's `accept-language` header until a match is found. This means that if the user's browser sends the `accept-language` header `en-GB`, the "lookup" algorithm will not find a match, resulting in the default locale being used.

In contrast, the "best fit" algorithm compares a _distance_ between the user's `accept-language` header and the available locales, while taking into consideration regional information. Due to this, the "best fit" algorithm is able to match `en-US` as the best-matching locale in this case.

</Details>

### Domain-based routing [#location-detection-domain]

Prefer to watch a video?

<CourseVideoBanner
  className="mt-2"
  href="https://learn.next-intl.dev/chapters/06-routing/06-domain-based"
  title="Domain-based routing"
/>

If you're using the [`domains`](/docs/routing/configuration#domains) setting, the middleware will match the request against the available domains to determine the best-matching locale. To retrieve the domain, the host is read from the `x-forwarded-host` header, with a fallback to `host` (hosting platforms typically provide these headers out of the box).

The locale is detected based on these priorities:

1. A locale prefix is present in the pathname (e.g. `ca.example.com/fr`)
2. A locale is stored in a cookie and is supported on the domain
3. A locale that the domain supports is matched based on the [`accept-language` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language)
4. As a fallback, the `defaultLocale` of the domain is used

Since the middleware is aware of all your domains, if a domain receives a request for a locale that is not supported (e.g. `en.example.com/fr`), it will redirect to an alternative domain that does support the locale.

**Example workflow:**

1. The user requests `us.example.com` and based on the `defaultLocale` of this domain, the `en` locale is matched.
2. The app renders `<Link locale="fr" href="/">Switch to French</Link>` to allow the user to change the locale to `fr`.
3. When the link is clicked, a request to `us.example.com/fr` is initiated.
4. The middleware recognizes that the user wants to switch to another domain and responds with a redirect to `ca.example.com/fr`.

## Matcher config

The middleware is intended to only run on pages, not on arbitrary files that you serve independently of the user locale (e.g. `/favicon.ico`).

A popular strategy is to match all routes that don't start with certain segments (e.g. `/_next`) and also none that include a dot (`.`) since these typically indicate static files. However, if you have some routes where a dot is expected (e.g. `/users/jane.doe`), you should explicitly provide a matcher for these.

```tsx filename="proxy.ts"
export const config = {
  // Matcher entries are linked with a logical "or", therefore
  // if one of them matches, the middleware will be invoked.
  matcher: [
    // Match all pathnames except for
    // - … if they start with `/api`, `/_next` or `/_vercel`
    // - … the ones containing a dot (e.g. `favicon.ico`)
    '/((?!api|_next|_vercel|.*\\..*).*)',

    // However, match all pathnames within `/users`, optionally with a locale prefix
    '/([\\w-]+)?/users/(.+)'
  ]
};
```

{/* Keep this in sync with `packages/next-intl/test/middleware/middleware.test.tsx` */}

Note that some third-party providers like [Vercel Analytics](https://vercel.com/analytics) typically use internal endpoints that are then rewritten to an external URL (e.g. `/_vercel/insights/view`). Make sure to exclude such requests from your middleware matcher so they aren't rewritten by accident.

## Composing other middlewares

By calling `createMiddleware`, you'll receive a function of the following type:

```tsx
function middleware(request: NextRequest): NextResponse;
```

If you need to incorporate additional behavior, you can either modify the request before the `next-intl` middleware receives it, modify the response or even create the middleware based on dynamic configuration.

```tsx filename="proxy.ts"
import createMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';

export default async function proxy(request: NextRequest) {
  // Step 1: Use the incoming request (example)
  const defaultLocale = request.headers.get('x-your-custom-locale') || 'en';

  // Step 2: Create and call the next-intl middleware (example)
  const handleI18nRouting = createMiddleware({
    locales: ['en', 'de'],
    defaultLocale
  });
  const response = handleI18nRouting(request);

  // Step 3: Alter the response (example)
  response.headers.set('x-your-custom-locale', defaultLocale);

  return response;
}

export const config = {
  // Match only internationalized pathnames
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};
```

### Example: Additional rewrites

If you need to handle rewrites apart from the ones provided by `next-intl`, you can call [`NextResponse.rewrite()`](https://nextjs.org/docs/app/api-reference/functions/next-response#rewrite) conditionally after the `next-intl` middleware has run.

This example rewrites requests for `/[locale]/profile` to `/[locale]/profile/new` if a special cookie is set.

```tsx filename="proxy.ts"
import createMiddleware from 'next-intl/middleware';
import {NextRequest, NextResponse} from 'next/server';
import {routing} from './i18n/routing';

const handleI18nRouting = createMiddleware(routing);

export default async function proxy(request: NextRequest) {
  let response = handleI18nRouting(request);

  // Additional rewrite when NEW_PROFILE cookie is set
  if (response.ok) {
    // (not for errors or redirects)
    const [, locale, ...rest] = new URL(
      response.headers.get('x-middleware-rewrite') || request.url
    ).pathname.split('/');
    const pathname = '/' + rest.join('/');

    if (
      pathname === '/profile' &&
      request.cookies.get('NEW_PROFILE')?.value === 'true'
    ) {
      response = NextResponse.rewrite(
        new URL(`/${locale}/profile/new`, request.url),
        {headers: response.headers}
      );
    }
  }

  return response;
}

export const config = {
  // Match only internationalized pathnames
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};
```

You may wish to customize this based on your routing configuration and use case.

### Example: Integrating with Clerk

[`@clerk/nextjs`](https://clerk.com/docs/references/nextjs/overview) provides a middleware that can be [combined](https://clerk.com/docs/references/nextjs/clerk-middleware#combine-middleware) with other middlewares like the one provided by `next-intl`. By combining them, the middleware from `@clerk/next` will first ensure protected routes are handled appropriately. Subsequently, the middleware from `next-intl` will run, potentially redirecting or rewriting incoming requests.

```tsx filename="proxy.ts"
import {clerkMiddleware, createRouteMatcher} from '@clerk/nextjs/server';
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';

const handleI18nRouting = createMiddleware(routing);

const isProtectedRoute = createRouteMatcher(['/:locale/dashboard(.*)']);

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) await auth.protect();

  return handleI18nRouting(req);
});

export const config = {
  // Match only internationalized pathnames
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};
```

(based on `@clerk/nextjs@^6.0.0`)

### Example: Integrating with Supabase Authentication

In order to use Supabase Authentication with `next-intl`, you need to combine the Supabase middleware with the one from `next-intl`.

You can do so by following the [setup guide from Supabase](https://supabase.com/docs/guides/auth/server-side/nextjs?router=app) and adapting the middleware utils to accept a response object that's been created by the `next-intl` middleware instead of creating a new one:

```tsx filename="utils/supabase/middleware.ts"
import {createServerClient} from '@supabase/ssr';
import {NextResponse, type NextRequest} from 'next/server';

export async function updateSession(
  request: NextRequest,
  response: NextResponse
) {
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({name, value}) =>
            request.cookies.set(name, value)
          );
          cookiesToSet.forEach(({name, value, options}) =>
            response.cookies.set(name, value, options)
          );
        }
      }
    }
  );

  const {
    data: {user}
  } = await supabase.auth.getUser();

  return response;
}
```

Now, we can integrate the Supabase middleware with the one from `next-intl`:

```tsx filename="proxy.ts"
import createMiddleware from 'next-intl/middleware';
import {type NextRequest} from 'next/server';
import {routing} from './i18n/routing';
import {updateSession} from './utils/supabase/middleware';

const handleI18nRouting = createMiddleware(routing);

export async function middleware(request: NextRequest) {
  const response = handleI18nRouting(request);

  // A `response` can now be passed here
  return await updateSession(request, response);
}

export const config = {
  // Match only internationalized pathnames
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};
```

(based on `@supabase/ssr@^0.5.0`)

### Example: Integrating with Auth.js (aka NextAuth.js) [#example-auth-js]

The [Auth.js](https://authjs.dev/) middleware requires an integration with their control flow to be compatible with other middlewares. The [success callback](https://next-auth.js.org/configuration/nextjs#wrap-middleware) can be used to run the `next-intl` middleware on authorized pages. However, public pages need to be treated separately.

For pathnames specified in the [`pages` object](https://next-auth.js.org/configuration/nextjs#pages) (e.g. `signIn`), Auth.js will skip the entire middleware and not run the success callback. Therefore, we have to detect these pages before running the Auth.js middleware and only run the `next-intl` middleware in this case.

```tsx filename="proxy.ts"
import {withAuth} from 'next-auth/middleware';
import createMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';
import {routing} from './i18n/routing';

const publicPages = ['/', '/login'];

const handleI18nRouting = createMiddleware(routing);

const authMiddleware = withAuth(
  // Note that this callback is only invoked if
  // the `authorized` callback has returned `true`
  // and not for pages listed in `pages`.
  function onSuccess(req) {
    return handleI18nRouting(req);
  },
  {
    callbacks: {
      authorized: ({token}) => token != null
    },
    pages: {
      signIn: '/login'
    }
  }
);

export default function proxy(req: NextRequest) {
  const publicPathnameRegex = RegExp(
    `^(/(${locales.join('|')}))?(${publicPages
      .flatMap((p) => (p === '/' ? ['', '/'] : p))
      .join('|')})/?$`,
    'i'
  );
  const isPublicPage = publicPathnameRegex.test(req.nextUrl.pathname);

  if (isPublicPage) {
    return handleI18nRouting(req);
  } else {
    return (authMiddleware as any)(req);
  }
}

export const config = {
  // Match only internationalized pathnames
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};
```

(based on `next-auth@^4.0.0`)

<Callout>

Have a look at the [`next-intl` with NextAuth.js example](/examples#app-router-next-auth) to explore a working setup.

</Callout>

## Usage without proxy / middleware (static export)

If you're using the [static export](https://nextjs.org/docs/app/building-your-application/deploying/static-exports) feature from Next.js (`output: 'export'`), a proxy / middleware will not run. You can use [prefix-based routing](/docs/routing/configuration#locale-prefix) nontheless to internationalize your app, but a few tradeoffs apply.

**Static export limitations:**

1. Using a locale prefix is required (same as [`localePrefix: 'always'`](/docs/routing/configuration#locale-prefix-always))
2. The locale can't be negotiated on the server (same as [`localeDetection: false`](/docs/routing/configuration#locale-detection))
3. You can't use [`pathnames`](/docs/routing/configuration#pathnames), as these require server-side rewrites
4. [Static rendering](/docs/routing/setup#static-rendering) is required

Additionally, other [limitations as documented by Next.js](https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features) will apply too.

If you choose this approach, you might want to enable a redirect at the root of your app:

```tsx filename="app/page.tsx"
import {redirect} from 'next/navigation';

// Redirect the user to the default locale when `/` is requested
export default function RootPage() {
  redirect('/en');
}
```

If you add such a root page at `app/page.tsx`, you need to add a root layout at `app/layout.tsx` as well, even if it's just passing `children` through:

```tsx filename="app/layout.tsx"
export default function RootLayout({children}) {
  return children;
}
```

## Troubleshooting

### "The proxy / middleware doesn't run for a particular page." [#middleware-not-running]

To resolve this, make sure that:

1. The [proxy / middleware](/docs/routing/setup#proxy) is set up in the correct file (e.g. `src/proxy.ts`).
2. Your [`matcher`](#matcher-config) correctly matches all routes of your application, including dynamic segments with potentially unexpected characters like dots (e.g. `/users/jane.doe`).
3. In case you're [composing other middlewares](#composing-other-middlewares), ensure that the middleware is called correctly.
4. In case you require static rendering, make sure to follow the [static rendering guide](/docs/routing/setup#static-rendering) instead of relying on hacks like [`force-static`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic).

### "My page content isn't localized despite the pathname containing a locale prefix." [#content-not-localized]

This is very likely the result of your [proxy / middleware not running](#middleware-not-running) on the request. As a result, a potential fallback from [`i18n/request.ts`](/docs/usage/configuration#i18n-request) might be applied.

### "Unable to find `next-intl` locale because the proxy / middleware didn't run on this request and no `locale` was returned in `getRequestConfig`." [#unable-to-find-locale]

If the middleware _is not_ expected to run on this request (e.g. because you're using a setup without locale-based routing, you should explicitly return a `locale` from [`getRequestConfig`](/docs/usage/configuration#i18n-request) to recover from this error.

If the middleware _is_ expected to run, verify that your [middleware is set up correctly](#middleware-not-running).

Note that `next-intl` will invoke the `notFound()` function to abort the render if no locale is available after `getRequestConfig` has run. You should consider adding a [`not-found` page](/docs/environments/error-files#not-foundjs) due to this.
